What
React v16.7.0-alpha 中第一次引入了 Hooks 的概念,在 v16.8.0 版本正式发布 React Hooks ,其带来的最大的变化在于给予了Function Component (函数式组件)类似组件生命周期的概念,扩大了 Function Component 的应用范围。
目前 Function Component 基本属于 dump 组件,Hooks 的出现使得 Function Component 有了自己的状态与业务逻辑,简单逻辑在自己内部处理即可,不再需要通过 Props 的传递,使简单逻辑组件抽离更加方便,也使使用者无需关心组件内部的逻辑,只关心 Hooks 组件返回的结果即可。
- 基本:
useState
、useEffect
、useContext
- 额外:
useCallback
、useReducer
、useMemo
、useRef
、useLayoutEffect
、useImperativeHandle
、useDebugValue
使用条件
- 仅从 React 功能组件调用 hooks,不要从常规 js 函数中调用 hooks。
- 只能在顶层调用,不能在 for 循环、if 条件判断、 *函数嵌套 *中使用。(Ps:React Hooks: 不是魔法只是数组 一文中有解释为什么 hooks 只能在顶层调用。)
环境条件
1、必须完整阅读一次React Hooks官方文档
英文文档:https://reactjs.org/docs/hooks-intro.html
中文文档:https://zh-hans.reactjs.org/docs/hooks-intro.html
其中重点必看hooks: useState、useReducer、useEffect、useCallback、useMemo
另外推荐阅读:
- Dan的《useEffect完全指南》
- 衍良同学的《React Hooks完全上手指南》
2、工程必须引入lint插件,并开启相应规则
lint插件:https://www.npmjs.com/package/eslint-plugin-react-hooks
必开规则:
1 | { |
其中, react-hooks/exhaustive-deps
至少 warn,也可以是error。建议全新的工程直接配 “error”,历史工程配 “warn”。
切记,本条是硬性条件。如果你的工程,当前没开启 hooks lint rule,请不要编写任何 hooks 代码。
如果对于某些场景,确实不需要「exhaustive-deps」,可在代码处加:
1 | // eslint-disable-next-line react-hooks/exhaustive-deps |
3、如若有发现hooks相关lint导致的warning,不要全局autofix
除了hooks外,正常的lint基本不会改变代码逻辑,只是调整编写规范。但是hooks的lint规则不同,
exhaustive-deps
的变化会导致代码逻辑发生变化,这极容易引发线上问题,所以对于hooks的waning,请不要做全局autofix操作。除非保证每处逻辑都做到了充分回归。
另外建议开启vscode的「autofix on save」。未来无论是什么问题,能把error与warning 尽量遏制在最开始的开发阶段,保证自测跟测试时就是符合规则的代码
Why
React Hooks 主要解决以下三个主要的问题:
- 代码重用:在 Hooks 出来之前,常见的代码重用方式是 HOC 和 render props,这两种方式带来的问题是:
- 你需要解构自己的组件,非常的笨重,同时会带来很深的组件嵌套。
- 难以重用和共享组件中的与状态相关的逻辑,造成产生很多巨大的组件。
- 复杂的组件逻辑:在 class 组件中,有许多的 lifecycle 函数,当我们的组件需要处理多个互不相关的 local state 时,每个生命周期函数中可能会包含着各种互不相关的逻辑在里面,最终导致逻辑复杂的组件难以开发与维护。
- class 组件的困惑:
- 复杂的模式,如渲染道具和高阶组件。
- 由于业务变动,函数组件不得不改为类组件
一个 React 项目,是由无数个大大小小的组件组合而成的。在 React 的世界中,组件是一等公民。而我们平时拆分组件的依据无非是:尽量的复用代码。组件是UI + 逻辑
的复用,但逻辑复用能力等于 0。虽然 React 提供了 HOC 与 Render Props 两种方式来解决逻辑复用的问题,但由于 renderProps 嵌套问题等原因,这种解法并没有让逻辑复用流行起来。
React Hooks 很好的解决了逻辑复用的问题,同时还解决了状态共享的问题,是继 render-props 和 higher-order components 之后的第三种状态共享方案,且不会产生 JSX 嵌套地狱问题。
对于 Hooks、Render Props 和高阶组件来说,它们都有各自的使用场景:
- Hooks:
- 替代 Class 的大部分用例,除了
getSnapshotBeforeUpdate
和componentDidCatch
还不支持。- 提取复用逻辑。除了有明确父子关系的,其他场景都可以使用 Hooks。
- Render Props:在组件渲染上拥有更高的自由度,可以根据父组件提供的数据进行动态渲染。适合有明确父子关系的场景。
- 高阶组件:适合用来做注入,并且生成一个新的可复用组件。适合用来写插件。
How
useState
useState(0) 返回是个数组形式,useState(0) 代表 count 的初始值是 0,useState 现阶段只能传入一个初始值。useState 类似 setState,你可以看做是异步的,但 useState 必须保证执行顺序一一致,React 为每一次的 useState 调用分配一个空间,通过 useState 调用顺序辨别各个空间。
1
2
3
4
5
6
7
8
9
10const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return (
<>
<h1>{count}</h1>
<button onClick={increment}>+</button>
</>
)
}
useEffect
如果我们需要在一些特定的生命周期或者值变化后做一些操作的话,必须借助
useEffect
的一些特性去实现。useState
产生的 changeState 方法并没有提供类似于setState
的第二个参数一样的功能,因此如果需要在 State 改变后执行一些方法,必须通过useEffect
实现。
useEffect
的代码既会在初始化时候执行,也会在后续每次 rerender 时执行,接受两个参数:第一个参数为副作用需要执行的回调,生成的回调方法可以返回一个函数(将在组件卸载时运行);第二个为该副作用监听的状态数组,当对应状态发生变动时会执行副作用,如果第二个参数为空,那么在每一个 State 变化时都会执行该副作用。
使用 useEffect 实现类组件中生命周期:
useEffect 如果第二个参数数组中的成员变量为空,则表示与该副作用相关联的状态为空,不管其他状态如何变动,该副作用都不会再次执行,即实现了 effect 只会在组件
componentDidMount
和componentWillUnmout
时期执行。1
2
3useEffect(() => {
// 每次componentDidMount会调用这里
},[])如果需要在 componentWillUnmount 需要执行一些事件,可以 return 返回时候进行操作。
1
2
3
4
5
6useEffect(() => {
//只有componentDidMount时候调用这里
return () => {
// componentWillUnmount
};
}, [])useEffect 模拟 componentDidpdate,该生命周期在每次页面更新后都会被调用,那么可以利用 useEffect 如果第二个参数为空,那么在每一个 State 变化时都会执行该副作用的特性。
1
2
3
4
5
6
7
8const mounted = useRef();
useEffect(() => {
if(!mounted.current){
mounted.current = true;
} else {
// 执行 componentDidpdate
}
})
useLayoutEffect
如果副作用是跟 DOM 相关的,需要使用 useLayoutEffect。useLayoutEffect 中的副作用会在 DOM 更新之后同步执行。
1 | function App() { |
useReducer
useReducer 接收两个参数,一个是 reducer 函数,跟 redux 中的 reducer 是一样的;另外一个是初始的状态值。返回的是一个数组,数组中的第一个元素是状态值,第二个元素是 dispatch 函数。
1 | const reducer = (state, action) => { |
useRef
useRef:在函数组件中获取组件或DOM节点的引用。
1 | function App() { |
还可以用useRef
来保存一些值的引用,并对它进行读写。举个例子:
1 | const useValues = () => { |
在使用 ref
时要特别小心,因为它可以随意赋值,所以一定要控制好修改它的方法。特别是一些底层模块,在封装的时候千万不要直接暴露 ref
,而是提供一些修改它的方法。
useContext、useReducer
Context 的作用就是对它所包含的组件树提供全局共享数据的一种技术。useReducer 结合 useContext,通过 context 把 dispatch 函数提供给组件树中的所有组件使用 ,而不用通过 props 添加回调函数的方式一层层传递。
1 | // 定义初始化值 |
注:局部状态不推荐使用 useReducer
,会导致函数内部状态过于复杂,难以阅读。 useReducer
建议在多组件间通信时,结合 useContext
一起使用。
参考 react-redux
useMemo
当状态发生变化时,没有设置关联状态的 useEffect
会全部执行。同样的,通过计算出来的值或者引入的组件也会重新计算/挂载一遍,即使与其关联的状态没有发生任何变化。
为了解决这个问题,引入了 useMemo
来实现类组件中 shouldComponetUpdate
的性能优化。 useMemo
接受两个参数,第一个参数为一个 Getter 方法,返回值为要缓存的数据或组件,第二个参数为该返回值相关联的状态,当其中任何一个状态发生变化时就会重新调用 Getter 方法生成新的返回值。
1 | import React, { useState, useMemo } from 'react'; |
useCallback
useCallback
用户生成 Callback ,配合useEffect
使用,抽离不同 useEffect
中存在的相同逻辑的封装,减少代码冗余。
useCallback
的使用方法和 useEffect
一致,第一个参数为生成的回调方法,第二个参数为该方法关联的状态,任一状态发生变动都会重新生成新的回调。
1 | const [count1, changeCount1] = useState(0); |
Custom Hooks
举个🌰,自定义监听窗口大小组件:
1 | // 一个显示目前窗口大小的组件 |
自定义 useWindowWidth, 窗口调整大小时使用副作用来设置状态:
1 | import { useState, useEffect} from 'react'; |
注: custom hooks 还可以参考一个有意思的 hooks 库 react-use。
原理
useState 底层实现
1 | let state = []; |
Hooks 下的数据流管理
社区有一个非常轻量的库 unstate-next ,可以做到管理全局的 state :
1 | import React, { useState } from "react" |